<?php

if (!defined('_CONST_COOKIE_SALT')) {
  define('_CONST_COOKIE_SALT', 'JgVvRyMX4yk2xcgxAxd51apmqVooWgu');
}

if (!defined('_CONST_VB_PREFIX')) {
  define('_CONST_VB_PREFIX', 'vb_');
}

if (!defined('_CONST_COOKIE_PREFIX')) {
  define('_CONST_COOKIE_PREFIX', 'mfvb_');
}

if (!defined('_CONST_TIMENOW')) {
  define('_CONST_TIMENOW', time());
}

if (!defined('_CONST_TIMEOUT')) {
  define('_CONST_TIMEOUT', 15*60);
}

define('TYPE_INT', 2); // force integer
define('TYPE_STR', 7); // force trimmed string

class Session
{
	var $vars = array();

	var $db_fields = array(
		'sessionhash'   => TYPE_STR,
		'userid'        => TYPE_INT,
		'host'          => TYPE_STR,
		'idhash'        => TYPE_STR,
		'lastactivity'  => TYPE_INT,
		'location'      => TYPE_STR,
		'styleid'       => TYPE_INT,
		'languageid'    => TYPE_INT,
		'loggedin'      => TYPE_INT,
		'inforum'       => TYPE_INT,
		'inthread'      => TYPE_INT,
		'incalendar'    => TYPE_INT,
		'badlocation'   => TYPE_INT,
		'useragent'     => TYPE_STR,
		'bypass'        => TYPE_INT,
		'profileupdate' => TYPE_INT,
		'apiclientid'   => TYPE_INT,
		'apiaccesstoken'=> TYPE_STR
	);

	var $changes = array();
	var $created = false;
	var $userinfo = null;

	var $db = null;

	function __construct(&$database, $sessionhash = '', $userid = 0, $password = '')
	{
		$this->db = $database;

		define('IPADDRESS',    $this->fetch_ip());
		define('ALT_IP',       $this->fetch_alt_ip());

		define('WOLPATH', $this->fetch_wolpath());

		define('SESSION_HOST', substr(IPADDRESS, 0, 15));
		define('SCRIPTPATH',   $this->fetch_scriptpath());

		define('USER_AGENT',   $_SERVER['HTTP_USER_AGENT']);
		define('REFERRER',     $_SERVER['HTTP_REFERER']);

		$userid = intval($userid);
		$gotsession = false;

		if (!defined('SESSION_IDHASH'))
		{
			define('SESSION_IDHASH', md5($_SERVER['HTTP_USER_AGENT'] . $this->fetch_substr_ip(ALT_IP))); // this should *never* change during a session
		}

		// sessionhash specified, so see if it already exists
		if ($sessionhash)
		{
			if ($session = $this->db->queryFirst("
				SELECT *
				FROM " . _CONST_VB_PREFIX . "session
				WHERE sessionhash = " . string_sql($sessionhash) . "
					AND lastactivity > " . (_CONST_TIMENOW - _CONST_TIMEOUT) . "
					AND idhash = " . string_sql(SESSION_IDHASH) . "
			", 300) AND $this->fetch_substr_ip($session['host']) == $this->fetch_substr_ip(SESSION_HOST))
			{
				$gotsession = true;
				$this->vars =& $session;
				$this->created = false;

				// found a session - get the userinfo
				if ($session['userid'] != 0)
				{
					$userinfo = fetch_userinfo($session['userid']);
					$this->userinfo =& $userinfo;
				}
			}
		}

		// or maybe we can use a cookie..
		if (($gotsession == false OR empty($session['userid'])) AND $userid AND $password)
		{
			$userinfo = fetch_userinfo($userid);

			if (md5($userinfo['password'] . _CONST_COOKIE_SALT) == $password)
			{
				$gotsession = true;

				// combination is valid
				if (!empty($session['sessionhash']))
				{
					// old session still exists; kill it
					$this->db->execute("
						DELETE FROM " . _CONST_VB_PREFIX . "session
						WHERE sessionhash = " . string_sql($session['sessionhash']). "
					");
				}

				$this->vars = $this->fetch_session($userinfo['userid']);
				$this->created = true;

				$this->userinfo =& $userinfo;
			}
		}

		// at this point, we're a guest, so lets try to *find* a session
		// you can prevent this check from being run by passing in a userid with no password
		if ($gotsession == false AND $userid == 0)
		{
			if ($session = $this->db->queryFirst("
				SELECT *
				FROM " . _CONST_VB_PREFIX . "session
				WHERE userid = 0
					AND host = " . string_sql(SESSION_HOST) . "
					AND idhash = " . string_sql(SESSION_IDHASH) . "
				LIMIT 1
			", 300))
			{
				$gotsession = true;

				$this->vars =& $session;
				$this->created = false;
			}
		}

		// well, nothing worked, time to create a new session
		if ($gotsession == false)
		{
			$gotsession = true;

			$this->vars = $this->fetch_session(0);
			$this->created = true;
		}

		$this->vars['dbsessionhash'] = $this->vars['sessionhash'];

		if ($this->created == false)
		{
			$this->set('useragent', USER_AGENT);
			$this->set('lastactivity', _CONST_TIMENOW);
			$this->set('location', WOLPATH);
		}
	}

	function save()
	{
		$cleaned = $this->build_query_array();

		// since the sessionhash can be blanked out, lets make sure we pull from "dbsessionhash"
		$cleaned['sessionhash'] = string_sql($this->vars['dbsessionhash']);

		if ($this->created == true)
		{
			/*insert query*/
			$this->db->execute("
				INSERT IGNORE INTO " . _CONST_VB_PREFIX . "session
					(" . implode(', ', array_keys($cleaned)) . ")
				VALUES
					(" . implode(', ', $cleaned) . ")
			");
		}
		else
		{
			// update query

			unset($this->changes['sessionhash']); // the sessionhash is not updateable
			$update = array();
			foreach ($cleaned AS $key => $value)
			{
				if (!empty($this->changes["$key"]))
				{
					$update[] = "$key = $value";
				}
			}

			if (sizeof($update) > 0)
			{
				// note that $cleaned['sessionhash'] has been escaped as necessary above!
				$this->db->execute("
					UPDATE " . _CONST_VB_PREFIX . "session
					SET " . implode(', ', $update) . "
					WHERE sessionhash = $cleaned[sessionhash]
				");
			}
		}

		$this->changes = array();
	}

	function &fetch_userinfo()
	{
		if ($this->userinfo)
		{
			// we already calculated this
			return $this->userinfo;
		}
		else if ($this->vars['userid'])
		{
			// user is logged in
			$this->userinfo = fetch_userinfo($this->vars['userid']);
			return $this->userinfo;
		}
		else
		{
			// guest setup
			$this->userinfo = array(
				'userid'         => 0,
				'usergroupid'    => 1,
				'username'       => (!empty($_REQUEST['username']) ? htmlspecialchars_uni($_REQUEST['username']) : ''),
				'password'       => '',
				'email'          => '',
				'lastactivity'   => $this->vars['lastactivity'],
				'maxposts'       => -1,
				'securitytoken'  => 'guest',
				'securitytoken_raw'  => 'guest'
			);

			return $this->userinfo;
		}
	}

	function do_lastvisit_update($lastvisit = 0, $lastactivity = 0)
	{
		// update last visit/activity stuff
		if ($this->vars['userid'] == 0)
		{
			// guest -- emulate last visit/activity for registered users by cookies
			if ($lastvisit)
			{
				// we've been here before
				$this->userinfo['lastvisit'] = intval($lastvisit);
				$this->userinfo['lastactivity'] = ($lastvisit ? intval($lastvisit) : _CONST_TIMENOW);

				// here's the emulation
				if (_CONST_TIMENOW - $this->userinfo['lastactivity'] > _CONST_TIMEOUT)
				{
					$this->userinfo['lastvisit'] = $this->userinfo['lastactivity'];

					set_cookie(_CONST_COOKIE_PREFIX.'lastvisit', $this->userinfo['lastactivity'], _CONST_TIMENOW + 604800);
				}
			}
			else
			{
				// first visit!
				$this->userinfo['lastactivity'] = _CONST_TIMENOW;
				$this->userinfo['lastvisit'] = _CONST_TIMENOW;

				set_cookie(_CONST_COOKIE_PREFIX.'lastvisit', _CONST_TIMENOW, _CONST_TIMENOW + 604800);
			}
			set_cookie(_CONST_COOKIE_PREFIX.'lastactivity', $lastactivity, _CONST_TIMENOW + 604800);
		}
		else
		{
			// registered user
			if (_CONST_TIMENOW - $this->userinfo['lastactivity'] > _CONST_TIMEOUT)
			{
				// see if session has 'expired' and if new post indicators need resetting
				$this->db->execute("
					UPDATE " . _CONST_VB_PREFIX . "user
					SET
						lastvisit = lastactivity,
						lastactivity = " . _CONST_TIMENOW . "
					WHERE userid = " . $this->userinfo['userid'] . "
				", 'lastvisit');

				$this->userinfo['lastvisit'] = $this->userinfo['lastactivity'];
			}
			else
			{
				// if this line is removed (say to be replaced by a cron job, you will need to change all of the 'online'
				// status indicators as they use $userinfo['lastactivity'] to determine if a user is online which relies
				// on this to be updated in real time.
				$this->db->execute("
					UPDATE " . _CONST_VB_PREFIX . "user
					SET lastactivity = " . _CONST_TIMENOW . "
					WHERE userid = " . $this->userinfo['userid'] . "
				", 'lastvisit');
			}
		}
	}

	function build_query_array()
	{
		$return = array();
		foreach ($this->db_fields AS $fieldname => $cleantype)
		{
			switch ($cleantype)
			{
				case TYPE_INT:
					$cleaned = intval($this->vars["$fieldname"]);
					break;
				case TYPE_STR:
				default:
					$cleaned = string_sql($this->vars["$fieldname"]);
			}
			$return["$fieldname"] = $cleaned;
		}

		return $return;
	}

	function set($key, $value)
	{
		if (!isset($this->vars["$key"]) OR $this->vars["$key"] != $value)
		{
			$this->vars["$key"] = $value;
			$this->changes["$key"] = true;
		}
	}

	function fetch_session($userid = 0)
	{
		$sessionhash = $this->fetch_sessionhash();
		set_cookie(_CONST_COOKIE_PREFIX.'sessionhash', $sessionhash);

		return array(
			'sessionhash'   => $sessionhash,
			'dbsessionhash' => $sessionhash,
			'userid'        => intval($userid),
			'host'          => SESSION_HOST,
			'idhash'        => SESSION_IDHASH,
			'lastactivity'  => _CONST_TIMENOW,
			'location'      => WOLPATH,
			'styleid'       => 0,
			'languageid'    => 0,
			'loggedin'      => intval($userid) ? 1 : 0,
			'inforum'       => 0,
			'inthread'      => 0,
			'incalendar'    => 0,
			'badlocation'   => 0,
			'profileupdate' => 0,
			'useragent'     => USER_AGENT,
			'bypass'        => 0
		);
	}

	function fetch_sessionhash()
	{
		return md5(uniqid(microtime(), true));
	}

	private function fetch_substr_ip($ip, $length = null)
	{
		if ($length === null OR $length > 3)
		{
			$length = 1;
		}
		return implode('.', array_slice(explode('.', $ip), 0, 4 - $length));
	}

	function fetch_ip()
	{
		return (getenv(HTTP_X_FORWARDED_FOR))
		? getenv(HTTP_X_FORWARDED_FOR)
		: getenv(REMOTE_ADDR);
	}

	function fetch_alt_ip()
	{
		$alt_ip = $_SERVER['REMOTE_ADDR'];

		if (isset($_SERVER['HTTP_CLIENT_IP']))
		{
			$alt_ip = $_SERVER['HTTP_CLIENT_IP'];
		}
		else if (isset($_SERVER['HTTP_X_FORWARDED_FOR']) AND preg_match_all('#\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}#s', $_SERVER['HTTP_X_FORWARDED_FOR'], $matches))
		{
			// try to avoid using an internal IP address, its probably a proxy
			$ranges = array(
				'10.0.0.0/8' => array(ip2long('10.0.0.0'), ip2long('10.255.255.255')),
				'127.0.0.0/8' => array(ip2long('127.0.0.0'), ip2long('127.255.255.255')),
				'169.254.0.0/16' => array(ip2long('169.254.0.0'), ip2long('169.254.255.255')),
				'172.16.0.0/12' => array(ip2long('172.16.0.0'), ip2long('172.31.255.255')),
				'192.168.0.0/16' => array(ip2long('192.168.0.0'), ip2long('192.168.255.255')),
			);
			foreach ($matches[0] AS $ip)
			{
				$ip_long = ip2long($ip);
				if ($ip_long === false OR $ip_long == -1)
				{
					continue;
				}

				$private_ip = false;
				foreach ($ranges AS $range)
				{
					if ($ip_long >= $range[0] AND $ip_long <= $range[1])
					{
						$private_ip = true;
						break;
					}
				}

				if (!$private_ip)
				{
					$alt_ip = $ip;
					break;
				}
			}
		}
		else if (isset($_SERVER['HTTP_FROM']))
		{
			$alt_ip = $_SERVER['HTTP_FROM'];
		}

		return $alt_ip;
	}

	function strip_sessionhash($string)
	{
		$string = preg_replace('/(s|sessionhash)=[a-z0-9]{32}?&?/', '', $string);
		return $string;
	}

	function xss_clean($var)
	{
		static
			$preg_find    = array('#^javascript#i', '#^vbscript#i'),
			$preg_replace = array('java script',   'vb script');

		return preg_replace($preg_find, $preg_replace, htmlspecialchars(trim($var)));
	}

	function fetch_scriptpath()
	{
		if ($_SERVER['REQUEST_URI'] OR $_ENV['REQUEST_URI'])
		{
			$scriptpath = $_SERVER['REQUEST_URI'] ? $_SERVER['REQUEST_URI'] : $_ENV['REQUEST_URI'];
		}
		else
		{
			if ($_SERVER['PATH_INFO'] OR $_ENV['PATH_INFO'])
			{
				$scriptpath = $_SERVER['PATH_INFO'] ? $_SERVER['PATH_INFO'] : $_ENV['PATH_INFO'];
			}
			else if ($_SERVER['REDIRECT_URL'] OR $_ENV['REDIRECT_URL'])
			{
				$scriptpath = $_SERVER['REDIRECT_URL'] ? $_SERVER['REDIRECT_URL'] : $_ENV['REDIRECT_URL'];
			}
			else
			{
				$scriptpath = $_SERVER['PHP_SELF'] ? $_SERVER['PHP_SELF'] : $_ENV['PHP_SELF'];
			}

			if ($_SERVER['QUERY_STRING'] OR $_ENV['QUERY_STRING'])
			{
				$scriptpath .= '?' . ($_SERVER['QUERY_STRING'] ? $_SERVER['QUERY_STRING'] : $_ENV['QUERY_STRING']);
			}
		}

		// in the future we should set $registry->script here too
		$quest_pos = strpos($scriptpath, '?');
		if ($quest_pos !== false)
		{
			$script = urldecode(substr($scriptpath, 0, $quest_pos));
			$scriptpath = $script . substr($scriptpath, $quest_pos);
		}
		else
		{
			$scriptpath = urldecode($scriptpath);
		}

		$scriptpath = $this->strip_sessionhash($scriptpath);
		$scriptpath = $this->xss_clean($scriptpath);

		return $scriptpath;
	}

	function fetch_wolpath()
	{
		$wolpath = $this->fetch_scriptpath();

		return $wolpath;
	}
}

$usercache = array();

function fetch_userinfo(&$userid)
{
	global $database, $usercache;

	$db = &$database;

	$userid = intval($userid);

	// return the cached result if it exists
	if (isset($usercache["$userid"]))
	{
		return $usercache["$userid"];
	}

	// no cache available - query the user
	$user = $db->queryFirst("SELECT u.userid, u.usergroupid, u.membergroupids, u.infractiongroupids, u.username, u.password, u.salt, u.pmtotal, u.pmunread, u.fbuserid, u.avatarid, u.avatarrevision, a.avatarpath, NOT ISNULL(ca.userid) AS hascustomavatar, ca.dateline AS avatardateline, ca.width AS avatarwidth, ca.height AS avatarheight, ca.height_thumb AS avatarheight_thumb, ca.width_thumb AS avatarwidth_thumb, ca.filedata_thumb AS avatarfiledata_thumb FROM " . _CONST_VB_PREFIX . "user u LEFT JOIN " . _CONST_VB_PREFIX . "avatar a ON (a.avatarid = u.avatarid) LEFT JOIN " . _CONST_VB_PREFIX . "customavatar ca ON (ca.userid = u.userid) WHERE u.userid = $userid", 300);
	if (!$user)
	{
		return false;
	}

	// make a username variable that is safe to pass through URL links
	$user['urlusername'] = urlencode(unhtmlspecialchars($user['username']));

	$user['securitytoken_raw'] = sha1($user['userid'] . sha1($user['salt']) . sha1(_CONST_COOKIE_SALT));
	$user['securitytoken'] = _CONST_TIMENOW . '-' . sha1(_CONST_TIMENOW . $user['securitytoken_raw']);

	$user['logouthash'] =& $user['securitytoken'];

	$usercache["$userid"] = $user;
	return $usercache["$userid"];
}

function unhtmlspecialchars($text, $doUniCode = false)
{
	if ($doUniCode)
	{
		$text = preg_replace('/&#([0-9]+);/esiU', "convert_int_to_utf8('\\1')", $text);
	}

	return str_replace(array('&lt;', '&gt;', '&quot;', '&amp;'), array('<', '>', '"', '&'), $text);
}

function htmlspecialchars_uni($text, $entities = true)
{
	if ($entities)
	{
		$text = preg_replace_callback(
			'/&((#([0-9]+)|[a-z]+);)?/si',
			'htmlspecialchars_uni_callback',
			$text
		);
	}
	else
	{
		$text = preg_replace(
			// translates all non-unicode entities
			'/&(?!(#[0-9]+|[a-z]+);)/si',
			'&amp;',
			$text
		);
	}

	return str_replace(
		// replace special html characters
		array('<', '>', '"'),
		array('&lt;', '&gt;', '&quot;'),
		$text
	);
}

function htmlspecialchars_uni_callback($matches)
{

	if (count($matches) == 1)
	{
		return '&amp;';
	}

	if (strpos($matches[2], '#') === false)
	{
		// &gt; like
		if ($matches[2] == 'shy')
		{
			return '&shy;';
		}
		else
		{
			return "&amp;$matches[2];";
		}
	}
	else
	{
		// Only convert chars that are in ISO-8859-1
		if (($matches[3] >= 32 AND $matches[3] <= 126)
			OR
			($matches[3] >= 160 AND $matches[3] <= 255))
		{
			return "&amp;#$matches[3];";
		}
		else
		{
			return "&#$matches[3];";
		}
	}
}

function convert_int_to_utf8($intval)
{
	$intval = intval($intval);
	switch ($intval)
	{
		// 1 byte, 7 bits
		case 0:
			return chr(0);
		case ($intval & 0x7F):
			return chr($intval);

		// 2 bytes, 11 bits
		case ($intval & 0x7FF):
			return chr(0xC0 | (($intval >> 6) & 0x1F)) .
				chr(0x80 | ($intval & 0x3F));

		// 3 bytes, 16 bits
		case ($intval & 0xFFFF):
			return chr(0xE0 | (($intval >> 12) & 0x0F)) .
				chr(0x80 | (($intval >> 6) & 0x3F)) .
				chr (0x80 | ($intval & 0x3F));

		// 4 bytes, 21 bits
		case ($intval & 0x1FFFFF):
			return chr(0xF0 | ($intval >> 18)) .
				chr(0x80 | (($intval >> 12) & 0x3F)) .
				chr(0x80 | (($intval >> 6) & 0x3F)) .
				chr(0x80 | ($intval & 0x3F));
	}
}

?>